﻿using System;
using System.Data;
using System.Globalization;
using System.IO;
using System.Text;
using System.Xml;


namespace BizElements.BusinessLayer
{
    #region Documentation.
    /// <summary>
    /// Saves the state of the search engine's filter which can later be recovered.
    /// </summary>
    /// <remarks><para>Properties currently included in the state:
    /// <list type="button">
    /// <item><see cref="ISearchEngineField.AddWithOr"/></item>
    /// <item><see cref="ISearchEngineField.BeginValue"/></item>
    /// <item><see cref="ISearchEngineField.EndValue"/></item>
    /// <item><see cref="ISearchEngineField.Negate"/></item>
    /// <item><see cref="IBindableField.Ordinal"/></item>
    /// <item><see cref="ISearchEngineField.SelectedValueSetKey"/></item>
    /// <item><see cref="ISearchEngineField.Values"/></item>
    /// </list></para>
    /// <para>To achieve compact size, only properties with non-default, non-null values are serialized.</para></remarks>
    #endregion
    public static class SearchEngineFieldsState
    {
        #region StateFormat enumeration.

        /// <summary>
        /// Format of the serialized state. 
        /// </summary>
        private enum StateFormat
        {
            /// <summary>
            /// Plain XML text.
            /// </summary>
            Xml,

            /// <summary>
            /// XML in which all special characters have been converted to Unicode hexadecimal values.
            /// For example, the characters &lt; and &gt; are encoded as %3c and %3e.
            /// </summary>
            HttpUnicode,

            /// <summary>
            /// XML converted to an UTF8 array of bytes and encoded with Base64 algorithm.
            /// </summary>
            Utf8BytesConvertedToBase64String
        }

        #endregion

        #region Serialize.

        /// <summary>
        /// Saves the state of a collection of <see cref="ISearchEngineField"/> objects.
        /// </summary>
        /// <param name="searchFields">Search fields whose state will be saved.</param>
        /// <returns>A string which represents search fields' state.</returns>
        /// <remarks>This method uses the <see cref="StateFormat.Xml"/> format.</remarks>
        public static string Serialize(ISearchEngineFieldCollection searchFields)
        {
            return Serialize(searchFields, StateFormat.Xml);
        }

        /// <summary>
        /// Saves the state of a collection of <see cref="ISearchEngineField"/> objects.
        /// </summary>
        /// <param name="searchFields">Search fields whose state will be saved.</param>
        /// <param name="format">Format of the serialized state.</param>
        /// <returns>A string which represents search fields' state.</returns>
        private static string Serialize(ISearchEngineFieldCollection searchFields, StateFormat format)
        {
            XmlDocument state = new XmlDocument();
            XmlElement xmlCollection = state.CreateElement("ISearchEngineFieldCollection");
            state.AppendChild(xmlCollection);

            // Serialize only non-nulls and non-default attributes.
            if (!string.IsNullOrEmpty(searchFields.Name))
                xmlCollection.SetAttribute("Name", searchFields.Name);

            if (!string.IsNullOrEmpty(searchFields.Description))
                xmlCollection.SetAttribute("Description", searchFields.Description);

            foreach (ISearchEngineField field in searchFields)
            {
                // Skip unset fields to reduce XML size.
                if (!FieldHasValues(field))
                    continue;

                XmlElement xmlField = state.CreateElement("ISearchEngineField");
                state.DocumentElement.AppendChild(xmlField);

                // Serialize only non-nulls and non-default values.
                xmlField.SetAttribute("BindingName", field.BindingName);
                if (field.BeginValue != null)
                    xmlField.SetAttribute("BeginValue", Convert.ToString(field.BeginValue, CultureInfo.InvariantCulture));

                if (field.EndValue != null)
                    xmlField.SetAttribute("EndValue", Convert.ToString(field.EndValue, CultureInfo.InvariantCulture));

                if (field.Negate)
                    xmlField.SetAttribute("Negate", Convert.ToString(field.Negate, CultureInfo.InvariantCulture));

                if (field.AddWithOr)
                    xmlField.SetAttribute("AddWithOr", Convert.ToString(field.AddWithOr, CultureInfo.InvariantCulture));

                if (field.SelectedTextSearchMode != TextSearchMode.Default)
                    xmlField.SetAttribute("SelectedTextSearchMode", Convert.ToString(field.SelectedTextSearchMode, CultureInfo.InvariantCulture));

                if (field.Ordinal != 0)
                    xmlField.SetAttribute("Ordinal", Convert.ToString(field.Ordinal, CultureInfo.InvariantCulture));

                if (!string.IsNullOrEmpty(field.SelectedValueSetKey))
                    xmlField.SetAttribute("SelectedValueSetKey", field.SelectedValueSetKey);

                if (field.Values != null && field.Values.Length > 0)
                {
                    foreach (object val in field.Values)
                    {
                        XmlElement xmlValue = state.CreateElement("Value");
                        xmlField.AppendChild(xmlValue);
                        xmlValue.InnerText = Convert.ToString(val, CultureInfo.InvariantCulture);
                    }
                }

                if (field.IsNull)
                    xmlField.SetAttribute("IsNull", Convert.ToString(field.IsNull, CultureInfo.InvariantCulture));
            }

            StringWriter sw = new StringWriter();
            state.WriteTo(new XmlTextWriter(sw));
            string xmlText = sw.GetStringBuilder().ToString();
            return EncodeXmlState(xmlText, format);
        }

        private static bool FieldHasValues(ISearchEngineField field)
        {
            bool hasValues = field.BeginValue != null || (field.Values != null && field.Values.Length > 0) || field.SelectedValueSetKey != null || field.IsNull;
            return hasValues;
        }

        #endregion

        #region Deserialize.

        /// <summary>
        /// Recovers the state of the <see cref="ISearchEngineField"/> objects to one stored in the provided string.
        /// </summary>
        /// <param name="fields">Search fields whose state will be recovered.</param>
        /// <param name="state">A string which represents search fields' state.</param>
        /// <remarks><para>The method automatically determines the <see cref="StateFormat.Xml"/> in which 
        /// the state is encoded.</para>
        /// <para>The method overwrites auto-populated and hidden fields if the values for them are specified 
        /// in the search fields' state.</para></remarks>
        /// <exception cref="FormatException">Generated if the method cannot determine the <see cref="StateFormat.Xml"/> in which the state is encoded.</exception>
        public static void Deserialize(ISearchEngineFieldCollection fields, string state)
        {
            StateFormat? format = DetermineFormat(state);
            if (format == null)
                throw new FormatException("Cannot determine format of the provided state.");

            Deserialize(fields, state, format.Value, true, true);
        }

        /// <summary>
        /// Recovers the state of the <see cref="ISearchEngineField"/> objects to one stored in the provided string.
        /// </summary>
        /// <param name="fields">Search fields whose state will be recovered.</param>
        /// <param name="state">A string which represents search fields' state.</param>
        /// <param name="overwriteAutoPopulatedFields">Indicates whether the search fields whose <see cref="ISearchEngineField.IsAutoPopulated"/> property is set to true may be overwritten or reset.</param>
        /// <param name="overwriteHiddenFields">Indicates whether the search fields whose <see cref="IBindableField.IsHidden"/> property is set to true may be overwritten or reset.</param>
        /// <remarks><para>The method automatically determines the <see cref="StateFormat.Xml"/> in which 
        /// the state is encoded.</para>
        /// <para>The method overwrites auto-populated and hidden fields if the values for them are specified 
        /// in the search fields' state.</para></remarks>
        /// <exception cref="FormatException">Generated if the method cannot determine the <see cref="StateFormat.Xml"/> in which the state is encoded.</exception>
        public static void Deserialize(ISearchEngineFieldCollection fields, string state, bool overwriteAutoPopulatedFields, bool overwriteHiddenFields)
        {
            StateFormat? format = DetermineFormat(state);
            if (format == null)
                throw new FormatException("Cannot determine format of the provided state.");

            Deserialize(fields, state, format.Value, overwriteAutoPopulatedFields, overwriteHiddenFields);
        }

        private static void Deserialize(ISearchEngineFieldCollection fields, string state, StateFormat format, bool overwriteAutoPopulatedFields, bool overwriteHiddenFields)
        {
            // TODO: Use XmlReader instead of DataSet. Faster and less prone to errors.

            string decodedXmlState = DecodeXmlState(state, format);
            DataSet fieldData = new DataSet();
            fieldData.ReadXml(new StringReader(decodedXmlState));

            // Parse collection attributes.
            DataTable tableISearchEngineFieldCollection = fieldData.Tables["ISearchEngineFieldCollection"];
            if (tableISearchEngineFieldCollection != null && tableISearchEngineFieldCollection.Rows.Count > 0)
            {
                int nameColidx = tableISearchEngineFieldCollection.Columns.IndexOf("Name");
                if (nameColidx >= 0)
                    fields.Name = tableISearchEngineFieldCollection.Rows[0][nameColidx] as string;

                int descriptionColidx = tableISearchEngineFieldCollection.Columns.IndexOf("Description");
                if (descriptionColidx >= 0)
                    fields.Description = tableISearchEngineFieldCollection.Rows[0][descriptionColidx] as string;
            }

            // Parse fields and values.
            DataTable tableISearchEngineField = fieldData.Tables["ISearchEngineField"];
            DataTable tableValue = fieldData.Tables.Contains("Value") ? fieldData.Tables["Value"] : null;
            DataRelation relFieldToValues = fieldData.Relations.Contains("ISearchEngineField_Value") ? fieldData.Relations["ISearchEngineField_Value"] : null;

            int bindingNameColidx = tableISearchEngineField.Columns.IndexOf("BindingName");
            int beginValueColidx = tableISearchEngineField.Columns.IndexOf("BeginValue");
            int endValueColidx = tableISearchEngineField.Columns.IndexOf("EndValue");
            int negateColidx = tableISearchEngineField.Columns.IndexOf("Negate");
            int addWithOrColidx = tableISearchEngineField.Columns.IndexOf("AddWithOr");
            int ordinalColidx = tableISearchEngineField.Columns.IndexOf("Ordinal");
            int selectedValueSetKeyColidx = tableISearchEngineField.Columns.IndexOf("SelectedValueSetKey");
            int textModeColidx = tableISearchEngineField.Columns.IndexOf("SelectedTextSearchMode");
            int valueColidx = (tableValue != null) ? tableValue.Columns.IndexOf("Value_Text") : -1;
            int isNullColidx = tableISearchEngineField.Columns.IndexOf("IsNull");

            foreach (DataRow rowField in tableISearchEngineField.Rows)
            {
                string bindingName = (string)rowField[bindingNameColidx];
                ISearchEngineField field = fields[bindingName];
                if (field == null)
                    continue;

                if (field.IsAutoPopulated && !overwriteAutoPopulatedFields)
                    continue;

                if (field.IsHidden && !overwriteHiddenFields)
                    continue;

                if (beginValueColidx >= 0)
                {
                    object beginValue = rowField[beginValueColidx];
                    field.BeginValue = (beginValue != DBNull.Value) ? ParseValue(field.DataType, (string)beginValue) : null;
                }
                else
                {
                    field.BeginValue = null;
                }

                if (endValueColidx >= 0)
                {
                    object endValue = rowField[endValueColidx];
                    field.EndValue = (endValue != DBNull.Value) ? ParseValue(field.DataType, (string)endValue) : null;
                }
                else
                {
                    field.EndValue = null;
                }

                if (negateColidx >= 0)
                {
                    bool negate;
                    try { negate = Convert.ToBoolean(rowField[negateColidx]); }
                    catch { negate = false; }
                    field.Negate = negate;
                }
                else
                {
                    field.Negate = false;
                }

                if (addWithOrColidx >= 0)
                {
                    bool addWithOr;
                    try { addWithOr = Convert.ToBoolean(rowField[addWithOrColidx]); }
                    catch { addWithOr = false; }
                    field.AddWithOr = addWithOr;
                }
                else
                {
                    field.AddWithOr = false;
                }

                if (ordinalColidx >= 0)
                {
                    int ordinal;
                    try { ordinal = Convert.ToInt32(rowField[ordinalColidx]); }
                    catch { ordinal = 0; }
                    field.Ordinal = ordinal;
                }
                else
                {
                    field.Ordinal = 0;
                }

                if (selectedValueSetKeyColidx >= 0)
                {
                    object selectedValueSetKey = rowField[selectedValueSetKeyColidx];
                    field.SelectedValueSetKey = (selectedValueSetKey != DBNull.Value) ? (string)selectedValueSetKey : null;
                }
                else
                {
                    field.SelectedValueSetKey = null;
                }

                if (textModeColidx >= 0)
                {
                    TextSearchMode? textMode = NumberParser.ParseEnum<TextSearchMode>(Convert.ToString(rowField[textModeColidx]));
                    field.SelectedTextSearchMode = (textMode != null) ? textMode.Value : TextSearchMode.Default;
                }
                else
                {
                    field.SelectedTextSearchMode = TextSearchMode.Default;
                }

                if (valueColidx >= 0 && relFieldToValues != null)
                {
                    DataRow[] valueRows = rowField.GetChildRows(relFieldToValues);
                    if (valueRows.Length > 0)
                    {
                        field.Values = new object[valueRows.Length];
                        for (int idxValue = 0; idxValue < field.Values.Length; idxValue++)
                        {
                            object val = valueRows[idxValue][valueColidx];
                            field.Values[idxValue] = (val != DBNull.Value) ? ParseValue(field.DataType, (string)val) : null;
                        }
                    }
                }
                else
                {
                    field.Values = null;
                }

                if (isNullColidx >= 0)
                {
                    bool isNull;
                    try { isNull = Convert.ToBoolean(rowField[isNullColidx]); }
                    catch { isNull = false; }
                    field.IsNull = isNull;
                }
                else
                {
                    field.IsNull = false;
                }
            }
        }

        private static object ParseValue(Type type, string xmlValue)
        {
            if (TypeUtil.IsText(type))
                return xmlValue;
            else if (TypeUtil.IsInteger(type))
                return long.Parse(xmlValue, CultureInfo.InvariantCulture);
            else if (TypeUtil.IsDate(type))
                return DateTime.Parse(xmlValue, CultureInfo.InvariantCulture);
            else if (TypeUtil.IsFloatingPointNumber(type))
                return decimal.Parse(xmlValue, CultureInfo.InvariantCulture);
            else
                return xmlValue;
        }

        #endregion

        #region Formats.

        private static string EncodeXmlState(string xmlState, StateFormat format)
        {
            switch (format)
            {
                case StateFormat.Xml:
                    return xmlState;
                case StateFormat.Utf8BytesConvertedToBase64String:
                    byte[] utf8BinaryText = new UTF8Encoding().GetBytes(xmlState);
                    return Convert.ToBase64String(utf8BinaryText, Base64FormattingOptions.None);
                case StateFormat.HttpUnicode:
                    return System.Web.HttpUtility.UrlEncodeUnicode(xmlState);
                default:
                    throw new ArgumentException("Unsupported format: " + format.ToString() + ".", "format");
            }
        }

        private static string DecodeXmlState(string encodedXmlState, StateFormat format)
        {
            switch (format)
            {
                case StateFormat.Xml:
                    return encodedXmlState;
                case StateFormat.Utf8BytesConvertedToBase64String:
                    byte[] utf8BinaryText = Convert.FromBase64String(encodedXmlState);
                    return new UTF8Encoding().GetString(utf8BinaryText);
                case StateFormat.HttpUnicode:
                    return System.Web.HttpUtility.UrlDecode(encodedXmlState);
                default:
                    throw new ArgumentException("Unsupported format: " + format.ToString() + ".", "format");
            }
        }

        private static StateFormat? DetermineFormat(string encodedXmlState)
        {
            StateFormat? format = null;
            if (encodedXmlState.StartsWith("<ISearchEngineFieldCollection"))
                format = StateFormat.Xml;

            if (format == null && encodedXmlState.StartsWith("%3cISearchEngineFieldCollection"))
                format = StateFormat.HttpUnicode;

            if (format == null)
            {
                try
                {
                    byte[] utf8BinaryText = Convert.FromBase64String(encodedXmlState);
                    string decodedXml = new UTF8Encoding().GetString(utf8BinaryText);
                    if (decodedXml.StartsWith("<ISearchEngineFieldCollection"))
                        format = StateFormat.Utf8BytesConvertedToBase64String;
                }
                catch { }
            }

            return format;
        }

        #endregion
    }
}